COMP3141 Software System Design and Implementation

COMP3141: Software System Design and Implementation

Term 2, 2023

Code and Notes (Week 10 Wednesday)

Table of Contents

1 Live code

This is all the code I wrote during the lecture. No guarantee that it makes any sense out of context.

{-# LANGUAGE DataKinds, KindSignatures, GADTs,
             StandaloneDeriving #-}
module W10 where

{-
data Answer = Yes | No deriving (Eq,Show)
 -}

-- GADT syntax:
data Answer :: * where -- Answer is a type of kind *, such that...
  Yes :: Answer        -- Yes is a constructor of type Answer
  No  :: Answer        -- No is a construcor type Answer
  deriving (Eq,Show) -- This *sometimes* works, but is not guaranteed
                     -- to be complete.
                     -- Or maybe it was added without Johannes noticing?
-- GADTs unfortunately do not support the `deriving` keyword
-- But: StandaloneDeriving allows us to write them stand-alone later

--deriving instance Eq Answer
--deriving instance Show Answer

{-
  data Sum a b = L a | R b

  ^ this is the same thing as Either

  data Either a b =
    Left a
  | Right b
 -}

-- We don't name the type arguments when we introduce the type constructor
data Sum :: * -> * -> * where
  L :: a -> Sum a b -- instead, we declare the type of each data constructor
  R :: b -> Sum a b

data Prod :: * -> * -> * where
  Pair :: a -> b -> Prod a b
  deriving Show

{- We're still doing things we can easily do without GADTs,
   but we're starting to see some places where the
   syntax diverges
 -}

{- Question: can we put arbitrary type variables
             in the types of L and R?
   Answer: Yes! That's one of the ways GADTs generalise datatypes
 -}
data Polarity = Positive | Negative | Zero
data Parity = Even | Odd

allTheThings :: [Sum Parity Polarity]
allTheThings =
  [L Even,
   L Odd,
   R Positive,
   R Negative,
   R Zero
   -- undefined -- these don't count
  ]

{- There's one striking fact about the number here:

      5 = 2 + 3

 -}
allTheOtherThings :: [Prod Parity Polarity]
allTheOtherThings =
  [Pair Even Positive,
   Pair Even Negative,
   Pair Even Zero,
   Pair Odd Positive,
   Pair Odd Negative,
   Pair Odd Zero
  ]

{- There's one striking fact about the number 6 here:

     6 = 2 * 3
 -}

{- Remember earlier in the course, we've seen:
   - plain old lists

      data [a] = [] | a:[a]

   - non-empty lists (lists that are non-empty by construction)

      data NonEmpty a = Single a | Cons a (NonEmpty a)

   The advantage of non-empty list is that we can define

      safeHead  :: NonEmpty a -> a
      safeTail  :: NonEmpty a -> [a]
                   ^^ the tail of a non-empty list isn't necessarily empty

   which are *total* functions.

  But: what if I wanted a type for:
   - lists with at least two elements?
   - a type specifically for empty lists

  GADTs + phantom types gives us a way to define *all* of these types
   in one go.

  First ingredient: type-level numbers.
   Numbers that only exist at compile time in the type system,
   and are not used at all at runtime.
 -}

data Nat = Z | S Nat
{- Z     represents 0
   S(n)  represents n+1

   S(S(Z)) represents 2

   Remember: we enabled DataKinds,
     which means that every datatype declaration
     also simultatenously defines a kind.

   Nat is a *kind*
   which is inhabited by countable many types:

       Z       :: Nat
       S Z     :: Nat
       S(S(Z)) :: Nat

   ^ The type Z is not inhabited by any values,
     so we can only use it as a phantom type.

     But we can use it to enforce requirements on length.
 -}

-- Sized lists: lists where the size of the list is encoded
-- in its type by a type-level natural number
data Vec :: Nat -> * -> * where
  Nil  :: Vec Z a
  Cons :: a -> Vec n a -> Vec (S n) a

deriving instance Eq a => Eq(Vec n a)
deriving instance Show a => Show(Vec n a)

-- This completely subsumes NonEmpty lists from before

safeHead :: Vec (S n) a -> a
safeHead (Cons x xs) = x
-- Even though we haven't written a case for Nil,
-- this pattern matching is nonetheless exhaustive.
-- [a] a list of arbitrary length
-- We know that the argument is a list of length
   -- n+1 for some n
safeTail :: Vec (S n) a -> Vec n a
safeTail (Cons x xs) = xs

{- Not only can we do type-safe head and tail
   We can do type-safe head-of-tail
 -}
sndElement :: Vec (S (S n)) a -> a
sndElement = safeHead . safeTail
--sndElement(Cons _ (Cons y _)) = y

{- Let's define map!
 -}
mapV :: (a -> b) -> Vec n a -> Vec n b
mapV f Nil = Nil
mapV f (Cons x xs) = Cons (f x) (mapV f xs)

{- zipV that enforces that both lists have the same length
 -}
zipV :: Vec n a -> Vec n b -> Vec n (a,b)
zipV Nil Nil = Nil
zipV (Cons x xs) (Cons y ys) = Cons (x,y) (zipV xs ys)


safeLast :: Vec (S n) a -> a
safeLast (Cons x Nil) = x
safeLast (Cons x xs) = safeLast xs

-- What's something that's difficult to do?
-- Let's (not) define append (aka ++)
-- We'd like to have this type signature:
--append :: Vec n a -> Vec m a -> Vec (n+m) a
-- but we don't have a definition of + for type-level numbers
-- you can make one, but it requires even more extensions

-- What's something you can't do?
-- We can't write this function:
--  filterV :: (a -> Bool) -> Vec n a -> Vec ??? a
-- We have no way of knowing statically how many
-- elements the return value has.
-- Therefore, filterV has to return a non-sized list:
--  filterV :: (a -> Bool) -> Vec n a -> [a]

{-  Previously, we would have proved by equational reasoning that

      length(map f xs) = length xs

    Now, we don't have to! The type of `mapV` already enforces  that
    the length of the argument and return vector are the same,
    because they're referring to the same number n

 -}


{- A type-safe RPN calculator
   *without* zero-padding
 -}

-- Intuitively: the type-level number means
--   the number of elements that must exist on the stack
--   in order to run this computation successfully
data RPN :: Nat -> * where
  Add      :: RPN (S n) -> RPN (S(S n))
  Enter    :: Int -> RPN (S n) -> RPN n
  Done     :: RPN (S Z) -- Done expects a 1-element stack
                        -- because the point of the calculation
                        -- is to compute a number
  AllClear :: RPN Z -> RPN n

calc' :: RPN n -> Vec n Int -> Int
calc' (Add i) (Cons x (Cons y s)) =
  calc' i (Cons (x+y) s)
calc' (Enter x i) s =
  calc' i (Cons x s)
calc' Done (Cons x _) = x
calc' (AllClear i) s =
  calc' i Nil

calc :: RPN Z -> Int
calc i = calc' i Nil

{-
  Enter 2 $ AllClear $ Enter 3 $ Enter 4 $ Add $ Done
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                        this calculation has type RPN Z
 -}

{- What classes should  you take if you want to pursue functional programming?


    3141

    3161 Concepts of Programming Languages,
          the other "not a Haskell course" course
          more focussed on foundations as opposed to programming
    to a lesser extent
    4161
     ^ more focussed on formal verification
        (proving programs correct)
       in a language that shares a lot of DNA with Haskell
 -}

2023-08-13 Sun 12:52

Announcements RSS